﻿using System;
using System.IO;
using System.Net;
using System.Windows;
using System.Windows.Resources;
using System.Reflection;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;

using SBPweb.Forge.Contracts.Events;

using SBPweb.PROvibe.Business.Shared.Contracts.Content;

namespace SBPweb.PROvibe.Business.Client.Library.Modularity
{

    public static class ModuleLoader
    {

        #region Private fields

        private static Dictionary<TypeDescriptor, ConstructorInfo> dictConstructors = new Dictionary<TypeDescriptor, ConstructorInfo>();
        private static Dictionary<Type, MulticastDelegate> dictHandlers = new Dictionary<Type, MulticastDelegate>();

        #endregion

        #region Public methods for instance creation

        public static void CreateInstance<TType>(string xapPath, string assemblyPath, string classFQName, EventHandler<ModuleLoaderObjectCreatedEventArgs<TType>> loadedHandler, object userState, params object[] constructorParams)
        {
            CreateInstance<TType>(new TypeDescriptor(xapPath, assemblyPath, classFQName), loadedHandler, userState, constructorParams);
        }

        public static void CreateInstance<TType>(TypeDescriptor typeDescriptor, EventHandler<ModuleLoaderObjectCreatedEventArgs<TType>> loadedHandler, object userState, params object[] constructorParams)
        {
            ConstructorInfo cI = GetFromCache(typeDescriptor);
            if (cI == null)
            {
                ObjectLoadDescriptor<TType> old = new ObjectLoadDescriptor<TType>(typeDescriptor, loadedHandler, userState, constructorParams);
                WebClient wc = new WebClient();
                wc.OpenReadCompleted += new OpenReadCompletedEventHandler(wc_OpenReadCompleted<TType>);
                wc.OpenReadAsync(new Uri(typeDescriptor.XapPath, UriKind.RelativeOrAbsolute), old);
            }
            else
            {
                ModuleLoaderObjectCreatedEventArgs<TType> args = new ModuleLoaderObjectCreatedEventArgs<TType>(default(TType));
                args.Object = (TType)cI.Invoke(constructorParams);
                if (loadedHandler != null)
                {
                    loadedHandler.DynamicInvoke(null, args);
                }
                OnInstanceCreated(dictConstructors, args.Object.GetType(), (object)args, args.Object, userState);
            }
        }

        public static void AddInstanceCreatedHandler<TType>(EventHandler<ModuleLoaderObjectCreatedEventArgs<TType>> createdHandler)
        {
            lock (dictHandlers)
            {
                Type type = typeof(TType);
                if (!dictHandlers.ContainsKey(type))
                {
                    dictHandlers.Add(type, createdHandler);
                }
                else
                {
                    dictHandlers[type] = (MulticastDelegate)MulticastDelegate.Combine(dictHandlers[type], createdHandler);
                }
            }
        }

        public static void RemoveInstanceCreatedHandler<TType>(EventHandler<ModuleLoaderObjectCreatedEventArgs<TType>> createdHandler)
        {
            lock (dictHandlers)
            {
                Type type = typeof(TType);
                if (!dictHandlers.ContainsKey(type))
                {
                    return;
                }
                MulticastDelegate deleg = dictHandlers[type];
                dictHandlers[type] = (MulticastDelegate)MulticastDelegate.Remove(deleg, createdHandler);
                if (deleg.GetInvocationList().Length < 1)
                {
                    dictHandlers.Remove(type);
                }
            }
        }

        private static void OnInstanceCreated(object sender, Type type, object args, object obj, object userState)
        {
            lock (dictHandlers)
            {
                foreach (Type t in dictHandlers.Keys)
                {
                    if (t.IsAssignableFrom(type))
                    {
                        Type typeEventArgs = args.GetType().GetGenericTypeDefinition();
                        Type typeGeneric = typeEventArgs.MakeGenericType(t);
                        dictHandlers[t].DynamicInvoke(sender, Activator.CreateInstance(typeGeneric, obj, userState));
                    }
                }
            }
        }

        #endregion

        #region Private helper funtions

        private static ConstructorInfo GetFromCache(TypeDescriptor descriptor)
        {
            lock (dictConstructors)
            {
                if (dictConstructors.ContainsKey(descriptor))
                {
                    return dictConstructors[descriptor];
                }
                return null;
            }
        }

        private static void PutToCache(TypeDescriptor descriptor, ConstructorInfo constructorInfo)
        {
            lock (dictConstructors)
            {
                dictConstructors[descriptor] = constructorInfo;
            }
        }

        private static void wc_OpenReadCompleted<TType>(object sender, OpenReadCompletedEventArgs e)
        {
            ObjectLoadDescriptor<TType> descriptor = (ObjectLoadDescriptor<TType>)e.UserState;
            ModuleLoaderObjectCreatedEventArgs<TType> args = new ModuleLoaderObjectCreatedEventArgs<TType>(default(TType));
            Type type = LoadTypeFromStream(e.Result, descriptor.AssemblyPath, descriptor.FullyQualifiedTypeName);

            List<Type> paramTypes = new List<Type>();
            foreach (object o in descriptor.ConstructorParameters)
            {
                paramTypes.Add(o.GetType());
            }
            if (type == null)
            {
                throw new TypeLoadException(string.Format("The type {1} could not be found in assembly: {0}.", descriptor.AssemblyPath, descriptor.FullyQualifiedTypeName));
            }
            ConstructorInfo cI = type.GetConstructor(paramTypes.ToArray());
            if (cI == null)
            {
                throw new TypeLoadException(string.Format("Constructor not found with given parameters for type {1} in assembly {0}.", descriptor.AssemblyPath, descriptor.FullyQualifiedTypeName));
            }
            try
            {
                args.Object = (TType)cI.Invoke(descriptor.ConstructorParameters);
                args.UserState = descriptor.UserState;
            }
            catch (TargetInvocationException ex)
            {
                throw ex.InnerException;
            }

            PutToCache(new TypeDescriptor(descriptor.XapPath, descriptor.AssemblyPath, descriptor.FullyQualifiedTypeName), cI);

            if (descriptor.LoadedHandler != null)
            {
                descriptor.LoadedHandler.DynamicInvoke(sender, args);
            }

            OnInstanceCreated(sender, args.Object.GetType(), (object)args, args.Object, args.UserState);
        }

        private static Type LoadTypeFromStream(Stream stream, string assemblyPath, string classFQName)
        {
            string appManifest = new StreamReader(Application.GetResourceStream(
                new StreamResourceInfo(stream, null),
                new Uri("AppManifest.xaml", UriKind.Relative)).Stream).ReadToEnd();

            XElement deploy = XDocument.Parse(appManifest).Root;
            List<XElement> parts = (from assemblyParts in deploy.Elements().Elements()
                                    select assemblyParts).ToList();

            Assembly asm = null;

            foreach (XElement xe in parts)
            {
                string source = xe.Attribute("Source").Value;

                AssemblyPart asmPart = new AssemblyPart();

                StreamResourceInfo streamInfo = Application.GetResourceStream(
                    new StreamResourceInfo(stream, "application/binary"),
                    new Uri(source, UriKind.Relative));

                if (source == assemblyPath)
                {
                    asm = asmPart.Load(streamInfo.Stream);
                }
                else if(streamInfo != null)
                {
                    asmPart.Load(streamInfo.Stream);
                }
            }
            if (asm == null)
            {
                throw new ArgumentException(string.Format("Assembly {0} was not found!", assemblyPath));
            }
            if (string.IsNullOrEmpty(classFQName))
            {
                MethodInfo mI = asm.EntryPoint;
                if (mI == null)
                {
                    throw new ArgumentException(string.Format("Entry point was not found for assembly {0}!", assemblyPath));
                }
                return mI.GetType();
            }
            return asm.GetType(classFQName);
        }

        #endregion

    }

}